Skip to content

Add 02-stage-audiodriver-2michat-v2 pi-gen stage for ReSpeaker V2.0 HAT#41

Open
genericJE wants to merge 5 commits intoflorian-asche:mainfrom
genericJE:feat/2michat-v2-stage
Open

Add 02-stage-audiodriver-2michat-v2 pi-gen stage for ReSpeaker V2.0 HAT#41
genericJE wants to merge 5 commits intoflorian-asche:mainfrom
genericJE:feat/2michat-v2-stage

Conversation

@genericJE
Copy link
Copy Markdown

@genericJE genericJE commented Apr 19, 2026

Summary

Adds a sibling stage to the existing 02-stage-audiodriver-2michat-v1, targeting the Seeed ReSpeaker 2-Mics Pi HAT V2.0. The V2.0 is a silent hardware revision: it replaces the WM8960 codec (I2C 0x1a) with a TLV320AIC3104 (I2C 0x18). Today the v1 stage's driver installer silently skips the overlay step on V2.0 hardware, producing a build that boots with no sound card.

Related: #22.

What the stage does

  • 01-driver/03-run-chroot.sh — builds respeaker-2mic-v2_0-overlay.dtbo from Seeed-Studio/seeed-linux-dtoverlays with dtc, installs it under /boot/overlays, and enables dtparam=i2c_arm=on + dtoverlay=respeaker-2mic-v2_0 in config.txt. No DKMS, no kernel headers — snd-soc-tlv320aic3x is already in the mainline kernel.

  • 02-set-audio-volume/files/configure_audio.sh — per-boot mixer tune. The TLV320 ships quiet on both ends: HP DAC at -23.5 dB, HP/Line amps at ~89% and muted on some units, and the capture-side PGA at 27% / 16 dB (too low for microWakeWord detection). Defaults applied:

  • 02-set-audio-volume/files/configure_audio.serviceType=oneshot, After=sound.target alsa-restore.service wireplumber.service, Requires=user@1000.service. Runs every boot because WirePlumber can reset ALSA state between sessions, so a first-boot-only guard would leave users stuck at the quiet defaults after the next reboot. Runtime amixer tweaks by the user are still possible — they just won't survive a reboot, same as the v1 / Respeaker Lite stages after fix(audiodriver): remove alsa setup use pipewire volume only #42.

  • .github/workflows/build-all.yml — adds build-2michat-v2 and build-2michat-v2-lva matrix jobs, and extends generate-rpi-imager-json.needs so the new images flow through to the RPI Imager JSON.

  • docs/hardware_2mic_v2.md + README row — hardware page + two README entries mirroring the v1 format.

Layout

Mirrors the v1 stage. Gap at 02-run.sh is intentional — V2 has no host-side setup (v1 used that slot to install the seeed-voicecard-v2 runtime helper that performs hardware detection on every boot; V2 doesn't need it).

02-stage-audiodriver-2michat-v2/
├── prerun.sh
├── 01-driver/
│   ├── 01-packages          (git build-essential device-tree-compiler alsa-utils)
│   └── 03-run-chroot.sh
└── 02-set-audio-volume/
    ├── 01-run.sh
    └── files/
        ├── configure_audio.sh
        └── configure_audio.service

Test plan

  • Manual procedure (overlay build, install, config.txt edits, mixer tuning) verified on three Pi Zero 2 W + ReSpeaker V2.0 boards
  • shellcheck clean on all scripts
  • Overlay build smoke-tested in a debian:bookworm arm64 container with the stage's declared package set
  • End-to-end pi-gen build validated on my fork's CI — all eight jobs (including build-2michat-v2 and build-2michat-v2-lva) green on the latest commit
  • Flashed the generated PiCompose_2MicHat-v2_Linux-Voice-Assistant.img.xz onto a fresh Pi Zero 2 W + V2.0 HAT: clean first-boot, drivers up, audio playback at HA default volume, "Hey Jarvis" wake-word detection working out-of-the-box (no manual mixer adjustment needed)

Commit stack

  • 4f4d110 — initial stage (overlay build, mixer oneshot, docs, README)
  • dbbf003 — self-audit: wire CI jobs, guard mixer service, drop dead wpctl call, trim 01-packages
  • a119130 — run mixer tune every boot and propagate amixer failures (WirePlumber resets ALSA state between sessions, so a first-boot guard leaves users stuck at quiet defaults after reboot)
  • 9121dd6 — align with fix(audiodriver): remove alsa setup use pipewire volume only #42: add wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0 + user@1000.service dependency so the user-scoped pipewire socket exists when the service runs
  • 138a088 — boost capture PGA from 27% (16 dB) to 80% (47.5 dB) for reliable microWakeWord detection; verified on satellite-bedroom that 27% produces zero wake-word triggers at normal speaking distance and 80% fixes it

@genericJE genericJE force-pushed the feat/2michat-v2-stage branch from eae3204 to d9d30a8 Compare April 19, 2026 01:22
@genericJE genericJE marked this pull request as draft April 19, 2026 01:31
@genericJE genericJE marked this pull request as ready for review April 19, 2026 02:02
Type=oneshot
ExecStart=/usr/bin/configure_audio.sh
ExecStartPost=/usr/bin/mkdir -p /var/lib/configure_audio
ExecStartPost=/usr/bin/touch /var/lib/configure_audio/success
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Services like wireplumber can set volume back to 0%. I suggest that this script runs on every boot.

@florian-asche
Copy link
Copy Markdown
Owner

Many thanks for your great work!

@florian-asche florian-asche added the enhancement New feature or request label Apr 19, 2026
genericJE added a commit to genericJE/PiCompose that referenced this pull request Apr 19, 2026
Address review feedback on florian-asche#41: drop the first-boot guard so the mixer
tuning runs on every boot. PipeWire / WirePlumber manage ALSA state per
session and can reset controls to 0% between reboots, which would leave
users stuck silent if the service only ran once.

Also make configure_audio.sh surface amixer set failures (previously
swallowed by unconditional return 0), so that a broken tuning exits
non-zero and the systemd Restart=on-failure path actually retries
instead of falsely claiming success and persisting the broken state
via alsactl store.

Verified with a shim-based test harness (5 cases: happy path, single
amixer set failure, missing control, multiple failures, card timeout).
The ReSpeaker 2-Mics Pi HAT V2.0 replaces the WM8960 codec (V1, I2C 0x1a)
with a TLV320AIC3104 codec (I2C 0x18). The mainline kernel already ships
the `snd-soc-tlv320aic3x` driver, so no DKMS module is needed — but a
device-tree overlay and a separate set of mixer defaults are required.
Without these two bits the card either never appears or appears silent.

The new stage mirrors the v1 layout:

- prerun.sh — copy_previous guard, same as v1
- 01-driver/
  - 01-packages — build deps only (no dkms, no kernel headers needed)
  - 03-run-chroot.sh — clone seeed-linux-dtoverlays, compile
    respeaker-2mic-v2_0-overlay.dtbo with dtc, install into /boot/overlays,
    enable I2C + dtoverlay=respeaker-2mic-v2_0 in /boot/config.txt
- 02-set-audio-volume/
  - 01-run.sh — install script + oneshot unit (same pattern as v1)
  - files/configure_audio.sh — TLV320-aware defaults: PCM 85%
    (digital pre-DAC, 100% clips on typical JST speakers), HP DAC and
    Line DAC to 100% (defaults sit at -23.5 dB), HP and Line output amps
    unmuted at 100%, then alsactl store + wpctl sink to 1.0
  - files/configure_audio.service — After=sound.target alsa-restore.service,
    no dependency on a seeed-voicecard.service (none needed for v2)

The v2 stage is additive; the existing v1 stage is untouched. A build
can pick v1 or v2 by including the matching directory in stage selection.
Follow-up to the initial 02-stage-audiodriver-2michat-v2 commit:

- build-all.yml: add build-2michat-v2 and build-2michat-v2-lva jobs and
  include them in generate-rpi-imager-json.needs. Without this the new
  stage exists but no image is ever produced.
- configure_audio.service: add ConditionPathExists=!/var/lib/configure_audio/success
  + ExecStartPost touch marker so mixer tuning runs once on first boot
  and subsequent manual amixer + alsactl store customisations survive
  reboots. Without the guard every boot reset the mixer to the stage's
  canned defaults.
- configure_audio.sh: drop the wpctl block. The service runs as root
  under systemd; wpctl requires the PipeWire user session (uid 1000)
  and the existing `|| true` made it a silent no-op.
- 01-driver/01-packages: drop i2c-tools (no runtime hardware detection
  on v2 — the overlay is fixed) and libasound2-plugins (v2 does not
  install a custom asound.conf that references resample plugins).
- docs/hardware_2mic_v2.md + README.md: add a v2 hardware page and
  two README rows (base + LVA) mirroring the v1 entries.
Address review feedback on florian-asche#41: drop the first-boot guard so the mixer
tuning runs on every boot. PipeWire / WirePlumber manage ALSA state per
session and can reset controls to 0% between reboots, which would leave
users stuck silent if the service only ran once.

Also make configure_audio.sh surface amixer set failures (previously
swallowed by unconditional return 0), so that a broken tuning exits
non-zero and the systemd Restart=on-failure path actually retries
instead of falsely claiming success and persisting the broken state
via alsactl store.

Verified with a shim-based test harness (5 cases: happy path, single
amixer set failure, missing control, multiple failures, card timeout).
@genericJE genericJE force-pushed the feat/2michat-v2-stage branch 3 times, most recently from 55eb107 to 65b9437 Compare April 19, 2026 07:44
@genericJE genericJE marked this pull request as draft April 19, 2026 07:48
…@1000 dep

Match the post-florian-asche#42 pattern already used by 02-stage-audiodriver-2michat-v1:
call `wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0` after the amixer tune so
the PipeWire default sink lands at unity, and add `After=/Wants=user@1000.service`
+ `Environment=XDG_RUNTIME_DIR=/run/user/1000` to the unit so wpctl can
reach the user PipeWire socket.

Drop the `PCM 85%` amixer set: PipeWire drives PCM as hardware volume
passthrough, so wpctl unity immediately overwrites whatever we set PCM
to. Verified on satellite-bedroom: `amixer set PCM 50%` then
`wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0` leaves PCM at 100%.
Keeping the line made the intent misleading ("85% for headroom") while
having zero effect on the final state.

The rest of the amixer tuning stays — unlike WM8960 on v1 or the
DSP-driven lite board, the TLV320AIC3104 on the V2.0 HAT ships with
`HP DAC` at -23.5 dB and `HP` at ~89%. Those stages are downstream of
the PipeWire-managed PCM, so wpctl unity on its own still yields a
stuck-quiet card.
@genericJE genericJE force-pushed the feat/2michat-v2-stage branch from 65b9437 to 9121dd6 Compare April 19, 2026 07:54
TLV320AIC3104 ships with the input PGA at 27% (16 dB), which is too
quiet for reliable microWakeWord triggering on a Pi Zero 2 W at typical
speaking distance. Verified on satellite-bedroom: at 27% 'Hey Jarvis'
never fires; at 80% (47.5 dB) it does. The output-side tune handled
audibility; this handles detectability.
@genericJE genericJE marked this pull request as ready for review April 19, 2026 10:32
@genericJE genericJE requested a review from florian-asche April 19, 2026 10:33
@genericJE
Copy link
Copy Markdown
Author

genericJE commented Apr 19, 2026

Related follow-up: linger user hardcoded to pi

Flagging this as a separate follow-up rather than rolling it into this PR (scope here is codec-only), but worth addressing.

While testing the v2 image on satellite-bedroom (fresh build from this stage, pi-gen user je rather than the default pi) the LVA container crashloops after logout:

Assertion 'o' failed at ../src/pulse/operation.c:67, function pa_operation_unref(). Aborting.
Creating PulseAudio cookie file at /run/user/1000/pulse/cookie
touch: cannot touch '/run/user/1000/pulse/cookie': No such file or directory

Root cause is 01-stage-picompose/03-install-pipewire-audio/02-run.sh:21:

touch /var/lib/systemd/linger/pi

The linger file is always created for pi, regardless of what the pi-gen first user actually is. When the runtime user differs, /run/user/$UID is torn down on logout, the ${LVA_XDG_RUNTIME_DIR}:${LVA_XDG_RUNTIME_DIR} bind mount in the LVA compose file collapses, and the cookie write fails.

Manual workaround (what I did to recover bedroom):

sudo loginctl enable-linger <actual-user>

Suggested fix

Use the pi-gen FIRST_USER_NAME variable with a fallback:

touch "/var/lib/systemd/linger/${FIRST_USER_NAME:-pi}"

FIRST_USER_NAME is set by pi-gen's top-level config and expanded by the host shell before the on_chroot heredoc runs, so the chroot receives the literal filename. getent passwd 1000 | cut -d: -f1 is a portable alternative if you'd rather not depend on the pi-gen var.

Addresses the symptom documented upstream in OHF-Voice/linux-voice-assistant#241 for all non-pi users. Also opened a small docs follow-up upstream to strengthen the wording of the already-documented linger step: OHF-Voice/linux-voice-assistant#299.

Happy to open a separate PR if you'd like.

@florian-asche
Copy link
Copy Markdown
Owner

I did changed the audio volume setup again. Can you adjust your changes one more time and test it. If you test input and output and make sure volume is at 100% i merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants